﻿/*
 * Created by SharpDevelop.
 * User: Corrado Mio
 * Date: 01/06/2012
 * Time: 20:20
 * 
 * 
 */
using System;
using System.Collections.Generic;
using System.Threading;

using CSharpExt.Threading;
using log4net;

namespace CSharpExt.Threading
{
    /// <summary>
    /// DelegateTimer e' una classe che utilizza un unico thread per processare
    /// l'elenco delle callback registrate.
    /// 
    /// L'idea e' quella di non dover creare un timer per ogni callback, ma avere
    /// un'unico oggetto che chiama le callback con un periodo piu' o meno costante, 
    /// ma per il quale la precisione non e' assolutamente un problema.
    /// 
    /// Il periodo minimo schedulabile e' di 1 second, anche se il periodo viene
    /// indicato in millisecondi.
    /// 
    /// E' possibile mandare in esecuzione la callback in un thread dedicato. Questo puo'
    /// essere utile se l'operazione schedulata richiede molto tempo, e questo 
    /// potrebbe ritardare eccessivamente le altre schedulazioni.
    /// 
    /// Se una callback eseguita in un thread separato e' ancora in esecuzione nella
    /// schedulazione successiva, la schedulazione viene saltata.
    /// </summary>
    public static class DelegateTimer
    {
        #region Private Structure
        
        class Info
        {
            internal Action<int, int> Callback; // callback ad chiamare
            internal int Period;            // ogni quanto tempo deve essere chiamata la
                                            // callback
            internal int LastCall;          // timestamp dell'ultima call
                                            //
            internal String Name;           // nome del thread se la callback deve essere
                                            // eseguita in un thread separato (in modo 
                                            // asincrono)
            internal bool Running;          // se l'elaborazione e' ancora in corso
                                            // alla schedulazione successiva
        }
        
        #endregion
        
        #region Private Data
        
        private static Thread _thread;
        private static List<Info> _callbacks = new List<Info>();
        
        #endregion
        
        #region Constructor
        
        static DelegateTimer()
        {
            // fa partire il thread utilizzato per la schdulazione
            _thread = new Thread(callCallbacks) { Name = "DelegateTimer" };
            _thread.Start();
        }
        
        #endregion
        
        #region Operations
        
        public static void Register(Action<int/*previous*/, int/*now*/> callback, int period)
        {
            Register(null, callback, period);
        }
        
        public static void Register(String n, Action<int/*previous*/, int/*now*/> callback, int period)
        {
            Info info = new Info()
            {
                Callback = callback, 
                Period   = period, 
                Name     = n, 
                LastCall = Environment.TickCount, 
            };
            
            lock(_callbacks)
            {
                _callbacks.Add(info);
            }
        }
        
        public static void Unregister(Action<int, int> callback)
        {
            lock(_callbacks)
            {
                Info selected = null;
                foreach(Info info in _callbacks)
                {
                    if(info.Callback == callback)
                    {
                        selected = info;
                        break;
                    }
                }
                
                if(selected != null)
                    _callbacks.Remove(selected);
            }
        }
        
        public static void Stop()
        {
            _thread.Interrupt();
        }
        
        #endregion
        
        #region Implementation
        
        private static void callCallbacks()
        {
            int start, now, delta, rest;
            
            while(true)
            {
                try
                {
                    start = Environment.TickCount;
                
                    foreach(Info info in _callbacks)
                    {
                        now = Environment.TickCount;
                        delta = now - info.LastCall;
                        
                        // non e' passato abbastanza tempo
                        if (delta < info.Period)
                            continue;
                        
                        // e' ancora in esecuzione
                        if (info.Running)
                            continue;
                        
                        // non deve essere chiamato in un thread separato
                        if(info.Name == null)
                        {
                            info.Callback(info.LastCall, now);
                            info.LastCall = now;
                        }
                        // deve essere chiamato in un thread separato
                        else
                        {
                            ThreadPools.QueueUserWorkItem(info.Name, asyncCallCallback, info);
                        }
                    }

                    // controlla se deve rimanere in attesa fino al prossimo
                    // secondo                    
                    {
                        delta =  (Environment.TickCount - start);
                        rest = 1000 - delta;
                        if (rest > 16)
                            Thread.Sleep(rest);
                    }
                }
                catch (ThreadAbortException)
                {
                    break;
                }
                catch(ThreadInterruptedException)
                {
                    break;
                }
                catch(Exception e)
                {
                    // qualunque altra eccezione, non e' un problema
                    LogManager.GetLogger("DelegateTimer").Error(e, e);
                }
            }
        }
        
        private static void asyncCallCallback(Object state)
        {
            int now = Environment.TickCount;
            Info info = (Info)state;
            
            try
            {
                info.Running = true;
                info.Callback(info.LastCall, now);
                info.LastCall = now;
            }
            catch (Exception e)
            {
                // qualunque altra eccezione, non e' un problema
                LogManager.GetLogger("DelegateTimer").Error(e, e);
            }
            finally
            {
                info.Running = false;
            }
        }
        
        #endregion
    }
}
